﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using log4net;
using System.Collections;
using System.Net;
using System.IO;
using LinqToTwitter;
using System.Text.RegularExpressions;
using Newtonsoft.Json;

namespace Version2
{
    /// <summary>
    /// This is a utility class that will connect to the Twitter API's and retrieve information.
    /// </summary>
    public class TwitterUtil
    {
        private ILog logger;
        private Dictionary<string, string> codedLocations;
        private List<SocialDataPoint> data;
        private TwitterSearchMetadata srchMetaData;
        private UserStream uStream;

        public TwitterUtil()
        {
            logger = LogManager.GetLogger(typeof(TwitterUtil));
            codedLocations = getCodes();
            srchMetaData = new TwitterSearchMetadata();
            data = null;
            uStream = null;
        }

        /// <summary>
        /// This class will set up our twitter stream to start listening to all SeattlePD accounts.
        /// 
        /// {"created_at":"Thu Mar 07 08:05:02 +0000 2013","id":309575402999934976,"id_str":"309575402999934976",
        /// "text":"Beat:E2, TRESPASS at 7XX BLOCK OF BROADWAY reported on 3\/6\/2013 11:41 PM, Call# 13000077307",
        /// "source":"\u003ca href=\"http:\/\/www.seattle.gov\" rel=\"nofollow\"\u003eSPD Beat E2\u003c\/a\u003e",
        /// "truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,
        /// "in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":747540632,"id_str":"747540632",
        /// "name":"SeattlePD Edward2","screen_name":"SeattlePDE2","location":"","url":"http:\/\/spdblotter.seattle.gov",
        /// "description":"News\/events from Seattle Police. This site is not monitored. Call 911 for emergencies. Comments, list of followers subject to public disclosure (RCW 42.56).",
        /// "protected":false,"followers_count":215,"friends_count":2,"listed_count":14,
        /// "created_at":"Thu Aug 09 16:46:59 +0000 2012","favourites_count":0,"utc_offset":null,"time_zone":null,
        /// "geo_enabled":false,"verified":false,"statuses_count":4602,"lang":"en","contributors_enabled":false,
        /// "is_translator":false,"profile_background_color":"393B3C",
        /// "profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/635797069\/9myd2a9hx8h9gsccxxwh.png",
        /// "profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/635797069\/9myd2a9hx8h9gsccxxwh.png",
        /// "profile_background_tile":false,"profile_image_url":"http:\/\/a0.twimg.com\/profile_images\/2514987777\/zntnl33fzan4hqg600l8_normal.png",
        /// "profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/2514987777\/zntnl33fzan4hqg600l8_normal.png",
        /// "profile_link_color":"2D76B9","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6",
        /// "profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,
        /// "default_profile_image":false,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,
        /// "coordinates":null,"place":null,"contributors":null,"retweet_count":0,"entities":{"hashtags":[],"urls":[],
        /// "user_mentions":[]},"favorited":false,"retweeted":false,"filter_level":"medium"}
        /// </summary>
        public void init(Action<object, EventArgs> callback)
        {
            logger.Info("Attempting to start streaming data.");
            TwitterContext twitterCtx = new TwitterContext(auth);
            string ids = "";

            Friendship friendship;
            string cursor = "-1";
            do
            {
                // Get all Nebby's friends.
                friendship =
                    (from friend in twitterCtx.Friendship
                     where friend.Type == FriendshipType.FriendsList &&
                           friend.ScreenName == "KingNebby" &&
                           friend.Cursor == cursor
                     select friend)
                    .SingleOrDefault();

                cursor = friendship.CursorMovement.Next;

                // Get only those with the right name.
                friendship.Users.ForEach(friend =>
                {
                    if (friend.Identifier.ScreenName.StartsWith("Seattle"))
                    {
                        ids += friend.Identifier.UserID + ",";
                    }
                });

            } while (cursor != "0");
            ids = ids.Substring(0, ids.Length - 1);
            logger.Debug(String.Format("All ids: \n{0}\n", ids));

            // Start streaming all their activity.
            uStream = (from stream in twitterCtx.UserStream
                              where stream.Type == UserStreamType.User
                              && stream.Follow == ids
                              select stream)
                              .StreamingCallback(stream =>
                    {
                        if (stream.Status == TwitterErrorStatus.RequestProcessingException)
                        {
                            logger.Warn(stream.Error.ToString());
                            return;
                        }
                        // Seems to fire a lot of blanks. Want to exclude those.
                        if (stream.Content.Length > 0)
                        {
                            var tweet = JsonConvert.DeserializeObject<dynamic>(stream.Content);
                            if (tweet == null || tweet.text == null || tweet.user == null)
                                return;
                            logger.Debug(String.Format("Received status: {0}, img_url:{1}, name:{2}, screen_name:{3}", tweet.text, tweet.user.profile_image_url, tweet.user.name, tweet.user.screen_name));
                            callback(new Status()
                            {
                                Text = tweet.text,
                                User = new User()
                                {
                                    Name = tweet.user.name,
                                    ScreenName = tweet.user.screen_name,
                                    ProfileImageUrl = tweet.user.profile_image_url
                                }
                            }, null);
                        }
                    }).SingleOrDefault();
        }

        /// <summary>
        /// Stops us from streaming live data.
        /// </summary>
        public void closeStream()
        {
            if (uStream == null)
                return;

            uStream.CloseStream();
            logger.Info("Closed the stream.");
        }

        // This should never be hardcoded! Need to move to an obfuscated properties file or something.
        private SingleUserAuthorizer auth = new SingleUserAuthorizer
        {
            Credentials = new InMemoryCredentials
            {
                ConsumerKey = @"TFFBLnE35TrHILtrH8wQtQ",
                ConsumerSecret = @"cU925O6BSTsB9r1M0oTelra2DXrPPAZlTg9V3EieLjE",
                AccessToken = @"ke2wP0XDBtG1YJFMt5aw44OKfkYegzBRhmEtPBn4",
                OAuthToken = @"872808954-qdGXOUlGI6KlERDy9d8hLApUl7jG515a0SB88Xmp"
            }
        };

        public enum SocialMedium
        {
            TWITTER,
            FACEBOOK,
        }

        /// <summary>
        /// This method will default to use twitter (and all avaliable sources). At the time of this writting
        /// we are only integrating with twitter.
        /// This should also forsee the valued data and filter it before returened. Again, this should take
        /// the burden of optimizing.
        /// </summary>
        /// <param name="sp"></param>
        /// <returns></returns>
        public List<SocialDataPoint> search(SearchParameters sp)
        {
            if (srchMetaData.canReturnSameData(sp))
            {
                logger.Debug("Search optimized! Returning same data.");
                return (from sd in data
                        where sd.BirthDate < sp.DateAndTime.AddMinutes(sp.MinutesBeforeAndAfter)
                        && sd.BirthDate > sp.DateAndTime.AddMinutes(-sp.MinutesBeforeAndAfter)
                        select sd).ToList();
            }
            data = getTweets(sp.Latitude, sp.Longitude, sp.Radius, sp.DateAndTime, sp.MinutesBeforeAndAfter, new SocialMedium[] { SocialMedium.TWITTER });
            if (data == null)
                return null;
            srchMetaData.updateLastSearchMetadata(sp, data[data.Count - 1], data[0]);
            // Should move filtering to here.
            return
                (from sd in data
                 where sd.BirthDate < sp.DateAndTime.AddMinutes(sp.MinutesBeforeAndAfter)
                 && sd.BirthDate > sp.DateAndTime.AddMinutes(-sp.MinutesBeforeAndAfter)
                 select sd).ToList();
        }

        /// <summary>
        /// Calls the twitter API with the given parameters and returns application specific objects.
        /// </summary>
        /// <param name="latitude"></param>
        /// <param name="longitude"></param>
        /// <param name="radius"></param>
        /// <param name="dayAndTime"></param>
        /// <param name="minutesBeforeAndAfter"></param>
        /// <param name="sources"></param>
        /// <returns></returns>
        public List<SocialDataPoint> getTweets(double latitude, double longitude, double radius,
            DateTime dayAndTime, double minutesBeforeAndAfter, SocialMedium[] sources)
        {
            int count = 0;
            if (!CheckCoordinates(latitude, longitude))
            {
                logger.Info("Could not use bad coordinates.");
                return null;
            }
            if (radius < 0 || radius > 100)
            {
                logger.Info("Radius out of valid bounds 0 - 100 miles.");
                return null;
            }
            if (dayAndTime < DateTime.Now.AddDays(-7))
            {
                logger.Info("Will not query Twitter for more than 7 days ago.");
                return null;
            }
            List<SocialDataPoint> data = new List<SocialDataPoint>();
            if (sources.Contains(SocialMedium.TWITTER))
            {
                // Right now we'll continue searching with just an empty search string.
                List<Status> relevantTweets = getTweetsForLocationAndRadius(latitude, longitude, radius, " ", dayAndTime, minutesBeforeAndAfter);
                if (relevantTweets == null)
                    return null;

                foreach (var rel in relevantTweets)
                {
                    double oLat = Double.NaN;
                    double oLon = Double.NaN;
                    string url = null;
                    Match match;
                    if ((match = Regex.Match(rel.Text, @".*(http://.*\s*).*")).Success)
                    {
                        url = match.Groups[1].Value;
                    }
                    if (rel.Coordinates != null && rel.Coordinates.Latitude != 0)
                    {
                        // *** Bug in LinqToTwitter ??? Lat and Long are swapped 
                        oLat = rel.Coordinates.Longitude;
                        oLon = rel.Coordinates.Latitude;
                        count++;
                    }
                    data.Add(new SocialDataPoint
                    {
                        Id = ulong.Parse(rel.StatusID),
                        Lat = oLat,
                        Lon = oLon,
                        BirthDate = rel.CreatedAt.ToLocalTime(),
                        Text = rel.Text,
                        PictureUrl = url,
                        User = new UserInfo { Name = rel.ScreenName, OtherInfo = rel.User.Name }
                    });
                }
            }
            if (sources.Contains(SocialMedium.FACEBOOK))
            {
                logger.Info("Cannot query Facebook; not implemented");
                return null;
            }
            if (data.Count == 0)
            {
                logger.Info(String.Format("Could not find any data with search params: lat={0} lon={1} rad={2} dayAndTime={3} mins={4}",
                    latitude, longitude, radius, dayAndTime, minutesBeforeAndAfter));
            }
            logger.Debug("Returning " + data.Count + " data points, geotagged=" + count);
            return data;
        }
        /// <summary>
        /// Will simply check that lat and lon are valid ranges.
        /// </summary>
        /// <param name="lat">the latitude</param>
        /// <param name="lon">the longitude</param>
        /// <returns>a boolean value if the parameters satisfy the conditions.</returns>
        public bool CheckCoordinates(double lat, double lon)
        {
            return lat > -90 && lat < 90 && lon > -180 && lon < 180;
        }

        /// <summary>
        /// This will search twitter with a location and a radius and keyword.
        /// We can also see when the last time was that we get back and try again starting from there.
        /// 
        /// This method will only go through 10 pages because we want to be careful of our twitter limit.
        /// Smart Since and Max Id calculations should be considered to cut down on number of pages we go through.
        /// 
        /// </summary>
        /// <param name="lat"></param>
        /// <param name="lon"></param>
        /// <param name="radius"></param>
        /// <param name="keyword"></param>
        /// <param name="min"></param>
        /// <returns></returns>
        private List<Status> getTweetsForLocationAndRadius(double lat, double lon, double radius, string keyword, DateTime dayAndTime, double min)
        {
            // This is our oldest time. Once we have a tweet that crosses this time, quit searching.
            DateTime since = dayAndTime.AddMinutes(-min).ToUniversalTime();
            DateTime until = dayAndTime.AddMinutes(min).ToUniversalTime();
            if (until > DateTime.Now.ToUniversalTime())
            {
                until = DateTime.Now.ToUniversalTime();
            }
            until = until.AddDays(1);

            // Can use our metadata to find out which intervals to search at?

            TwitterContext cntx = new TwitterContext(auth);

            logger.Debug("Twitter allows only 'day' searching. Searching from: " + since.ToLocalTime() + " to " + until.ToLocalTime());
            ulong max = ulong.MaxValue;
            Search srch = null;
            List<Status> result = new List<Status>();
            int pages = 1;

            do
            {
                srch =
                   (from search in cntx.Search
                    where search.Type == SearchType.Search
                          && search.Query == keyword
                          && search.Count == 100
                          && search.GeoCode == String.Format("{0},{1},{2}mi", lat, lon, radius)
                          && search.Until == until.Date
                          && search.MaxID == max
                    select search)
                   .SingleOrDefault();

                if (srch != null && srch.Count > 0)
                {
                    result.AddRange(srch.Statuses);
                    // Says to start at oldest for next search (page).
                    max = ulong.Parse(srch.Statuses[srch.Statuses.Count - 1].StatusID) - 1;
                }
                logger.Debug("Searching page: " + pages);
                pages++;
            } while (srch != null && result[result.Count - 1].CreatedAt > since && pages < 20);
            if (srch == null)
            {
                logger.Debug("Could not complete request. Time could be incorrect.");
                return null;
            }
            logger.Debug("Found a total of " + srch.Statuses.Count + " results.");
            logger.Debug("Latest: " + result[0].CreatedAt.ToLocalTime() + " Earliest: " + result[result.Count - 1].CreatedAt.ToLocalTime());
            return result;
        }

        /// <summary>
        /// Gets geocoded locations.
        /// </summary>
        /// <returns></returns>
        public Dictionary<string, string> getCodes()
        {
            if (codedLocations == null)
            {
                codedLocations = new Dictionary<string, string>();
            }
            return codedLocations;
        }
    }
}
